﻿// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Caching;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Utils;
using osuTK;
using osuTK.Input;

namespace osu.Framework.Graphics.Containers
{
    public abstract partial class ScrollContainer<T> : Container<T>, IScrollContainer, DelayedLoadWrapper.IOnScreenOptimisingContainer, IKeyBindingHandler<PlatformAction>
        where T : Drawable
    {
        /// <summary>
        /// Determines whether the scroll dragger appears on the left side. If not, then it always appears on the right side.
        /// </summary>
        public Anchor ScrollbarAnchor
        {
            get => Scrollbar.Anchor;
            set
            {
                Scrollbar.Anchor = value;
                Scrollbar.Origin = value;
                updatePadding();
            }
        }

        private bool scrollbarVisible = true;

        /// <summary>
        /// Whether the scrollbar is visible.
        /// </summary>
        public bool ScrollbarVisible
        {
            get => scrollbarVisible;
            set
            {
                scrollbarVisible = value;
                scrollbarCache.Invalidate();
            }
        }

        protected readonly ScrollbarContainer Scrollbar;

        private bool scrollbarOverlapsContent = true;

        /// <summary>
        /// Whether the scrollbar overlaps the content or resides in its own padded space.
        /// </summary>
        public bool ScrollbarOverlapsContent
        {
            get => scrollbarOverlapsContent;
            set
            {
                scrollbarOverlapsContent = value;
                updatePadding();
            }
        }

        /// <summary>
        /// Size of available content (i.e. everything that can be scrolled to) in the scroll direction.
        /// </summary>
        public float AvailableContent => ScrollContent.DrawSize[ScrollDim];

        /// <summary>
        /// Size of the viewport in the scroll direction.
        /// </summary>
        public float DisplayableContent => ChildSize[ScrollDim];

        /// <summary>
        /// Controls the distance scrolled per unit of mouse scroll.
        /// </summary>
        public float ScrollDistance = 80;

        /// <summary>
        /// This limits how far out of clamping bounds we allow the target position to be at most.
        /// Effectively, larger values result in bouncier behavior as the scroll boundaries are approached
        /// with high velocity.
        /// </summary>
        public float ClampExtension = 500;

        /// <summary>
        /// This corresponds to the clamping force. A larger value means more aggressive clamping. Default is 0.012.
        /// </summary>
        private const double distance_decay_clamping = 0.012;

        /// <summary>
        /// Controls the rate with which the target position is approached after ending a drag. Default is 0.0035.
        /// </summary>
        public double DistanceDecayDrag = 0.0035;

        /// <summary>
        /// Controls the rate with which the target position is approached after scrolling. Default is 0.01
        /// </summary>
        public double DistanceDecayScroll = 0.01;

        /// <summary>
        /// Controls the rate with which the target position is approached after jumping to a specific location. Default is 0.01.
        /// </summary>
        public double DistanceDecayJump = 0.01;

        /// <summary>
        /// Controls the rate with which the target position is approached. It is automatically set after
        /// dragging or scrolling.
        /// </summary>
        private double distanceDecay;

        /// <summary>
        /// The current scroll position.
        /// </summary>
        public double Current { get; private set; }

        /// <summary>
        /// The target scroll position which is exponentially approached by current via a rate of distance decay.
        /// </summary>
        /// <remarks>
        /// When not animating scroll position, this will always be equal to <see cref="Current"/>.
        /// </remarks>
        public double Target { get; private set; }

        /// <summary>
        /// The maximum distance that can be scrolled in the scroll direction.
        /// </summary>
        public double ScrollableExtent => Math.Max(AvailableContent - DisplayableContent, 0);

        /// <summary>
        /// The maximum distance that the scrollbar can move in the scroll direction.
        /// </summary>
        /// <remarks>
        /// May not be accurate to actual display of scrollbar if <see cref="ToScrollbarPosition"/> or <see cref="FromScrollbarPosition"/> are overridden.
        /// </remarks>
        protected double ScrollbarMovementExtent => Math.Max(DisplayableContent - Scrollbar.DrawSize[ScrollDim], 0);

        /// <summary>
        /// Clamp a value to the available scroll range.
        /// </summary>
        /// <param name="position">The value to clamp.</param>
        /// <param name="extension">An extension value beyond the normal extent.</param>
        protected double Clamp(double position, double extension = 0) => Math.Max(Math.Min(position, ScrollableExtent + extension), -extension);

        protected override Container<T> Content => ScrollContent;

        /// <summary>
        /// Whether we are currently scrolled to the beginning of content.
        /// </summary>
        /// <param name="lenience">How close to the extent we need to be.</param>
        public bool IsScrolledToStart(float lenience = Precision.FLOAT_EPSILON) => Precision.AlmostBigger(0, Target, lenience);

        /// <summary>
        /// Whether we are currently scrolled as far as possible into the scroll direction.
        /// </summary>
        /// <param name="lenience">How close to the extent we need to be.</param>
        public bool IsScrolledToEnd(float lenience = Precision.FLOAT_EPSILON) => Precision.AlmostBigger(Target, ScrollableExtent, lenience);

        /// <summary>
        /// The container holding all children which are getting scrolled around.
        /// </summary>
        public Container<T> ScrollContent { get; }

        protected virtual bool IsDragging { get; private set; }

        public bool IsHandlingKeyboardScrolling
        {
            get
            {
                if (IsHovered)
                    return true;

                InputManager inputManager = GetContainingInputManager();
                return inputManager != null && ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
            }
        }

        public Direction ScrollDirection { get; }

        /// <summary>
        /// The direction in which scrolling is supported, converted to an int for array index lookups.
        /// </summary>
        protected int ScrollDim => ScrollDirection == Direction.Horizontal ? 0 : 1;

        private readonly LayoutValue<IScrollContainer> parentScrollContainerCache = new LayoutValue<IScrollContainer>(Invalidation.Parent);

        [CanBeNull]
        private IScrollContainer parentScrollContainer => parentScrollContainerCache.IsValid
            ? parentScrollContainerCache.Value
            : parentScrollContainerCache.Value = this.FindClosestParent<IScrollContainer>();

        /// <summary>
        /// Creates a scroll container.
        /// </summary>
        /// <param name="scrollDirection">The direction in which should be scrolled. Can be vertical or horizontal. Default is vertical.</param>
        protected ScrollContainer(Direction scrollDirection = Direction.Vertical)
        {
            ScrollDirection = scrollDirection;

            Masking = true;

            Axes scrollAxis = scrollDirection == Direction.Horizontal ? Axes.X : Axes.Y;
            AddRangeInternal(new Drawable[]
            {
                ScrollContent = new Container<T>
                {
                    RelativeSizeAxes = Axes.Both & ~scrollAxis,
                    AutoSizeAxes = scrollAxis,
                },
                Scrollbar = CreateScrollbar(scrollDirection)
            });

            Scrollbar.Hide();
            Scrollbar.Dragged = onScrollbarMovement;
            ScrollbarAnchor = scrollDirection == Direction.Vertical ? Anchor.TopRight : Anchor.BottomLeft;

            AddLayout(parentScrollContainerCache);
        }

        private float lastUpdateDisplayableContent = -1;
        private float lastAvailableContent = -1;

        private void updateSize()
        {
            // ensure we only update scrollbar when something has changed, to avoid transform helpers resetting their transform every frame.
            // also avoids creating many needless Transforms every update frame.
            if (lastAvailableContent != AvailableContent || lastUpdateDisplayableContent != DisplayableContent)
            {
                lastAvailableContent = AvailableContent;
                lastUpdateDisplayableContent = DisplayableContent;
                scrollbarCache.Invalidate();
            }
        }

        private readonly Cached scrollbarCache = new Cached();

        private void updatePadding()
        {
            if (scrollbarOverlapsContent || !Precision.DefinitelyBigger(AvailableContent, DisplayableContent, 1f))
                ScrollContent.Padding = new MarginPadding();
            else
            {
                if (ScrollDirection == Direction.Vertical)
                {
                    ScrollContent.Padding = ScrollbarAnchor == Anchor.TopLeft
                        ? new MarginPadding { Left = Scrollbar.Width + Scrollbar.Margin.Left }
                        : new MarginPadding { Right = Scrollbar.Width + Scrollbar.Margin.Right };
                }
                else
                {
                    ScrollContent.Padding = ScrollbarAnchor == Anchor.TopLeft
                        ? new MarginPadding { Top = Scrollbar.Height + Scrollbar.Margin.Top }
                        : new MarginPadding { Bottom = Scrollbar.Height + Scrollbar.Margin.Bottom };
                }
            }
        }

        protected override bool OnDragStart(DragStartEvent e)
        {
            if (IsDragging || e.Button != MouseButton.Left || Content.AliveInternalChildren.Count == 0)
                return false;

            if (parentScrollContainer != null && parentScrollContainer.ScrollDirection != ScrollDirection)
            {
                bool dragWasMostlyHorizontal = Math.Abs(e.Delta.X) > Math.Abs(e.Delta.Y);
                if (dragWasMostlyHorizontal != (ScrollDirection == Direction.Horizontal))
                    return false;
            }

            lastDragTime = Time.Current;
            averageDragDelta = averageDragTime = 0;

            IsDragging = true;

            dragButtonManager = GetContainingInputManager().AsNonNull().GetButtonEventManagerFor(e.Button);

            return true;
        }

        protected override bool OnKeyDown(KeyDownEvent e)
        {
            if (IsHandlingKeyboardScrolling && !IsDragging)
            {
                switch (e.Key)
                {
                    case Key.PageUp:
                        OnUserScroll(Target - DisplayableContent);
                        return true;

                    case Key.PageDown:
                        OnUserScroll(Target + DisplayableContent);
                        return true;
                }
            }

            return base.OnKeyDown(e);
        }

        protected override bool OnMouseDown(MouseDownEvent e)
        {
            if (IsDragging || e.Button != MouseButton.Left)
                return false;

            // Continue from where we currently are scrolled to.
            Target = Current;
            return false;
        }

        // We keep track of this because input events may happen at different intervals than update frames
        // and we are interested in the time difference between drag _input_ events.
        private double lastDragTime;

        // These keep track of a sliding average (w.r.t. time) of the time between drag events
        // and the delta of drag events. Both of these moving averages are decayed at the same
        // rate and thus the velocity remains constant across time. The overall magnitude
        // of averageDragTime and averageDragDelta simple decreases such that more recent movements
        // have a larger weight.
        private double averageDragTime;
        private double averageDragDelta;

        private MouseButtonEventManager dragButtonManager;

        private bool dragBlocksClick;

        public override bool DragBlocksClick => dragBlocksClick;

        protected override void OnDrag(DragEvent e)
        {
            Trace.Assert(IsDragging, "We should never receive OnDrag if we are not dragging.");

            double currentTime = Time.Current;
            double timeDelta = currentTime - lastDragTime;
            double decay = Math.Pow(0.95, timeDelta);

            averageDragTime = averageDragTime * decay + timeDelta;
            averageDragDelta = averageDragDelta * decay - e.Delta[ScrollDim];

            lastDragTime = currentTime;

            Vector2 childDelta = ToLocalSpace(e.ScreenSpaceMousePosition) - ToLocalSpace(e.ScreenSpaceLastMousePosition);

            double scrollOffset = -childDelta[ScrollDim];
            double clampedScrollOffset = Clamp(Target + scrollOffset) - Clamp(Target);

            // If we are dragging past the extent of the scrollable area, half the offset
            // such that the user can feel it.
            scrollOffset = clampedScrollOffset + (scrollOffset - clampedScrollOffset) / 2;

            // similar calculation to what is already done in MouseButtonEventManager.HandlePositionChange
            // handles the case where a drag was triggered on an axis we are not interested in.
            // can be removed if/when drag events are split out per axis or contain direction information.
            dragBlocksClick |= Math.Abs(e.MouseDownPosition[ScrollDim] - e.MousePosition[ScrollDim]) > dragButtonManager.ClickDragDistance;

            scrollByOffset(scrollOffset, false);
        }

        protected override void OnDragEnd(DragEndEvent e)
        {
            Trace.Assert(IsDragging, "We should never receive OnDragEnd if we are not dragging.");

            dragBlocksClick = false;
            dragButtonManager = null;
            IsDragging = false;

            if (averageDragTime <= 0.0)
                return;

            double velocity = averageDragDelta / averageDragTime;

            // Detect whether we halted at the end of the drag and in fact should _not_
            // perform a flick event.
            const double velocity_cutoff = 0.1;
            if (Math.Abs(Math.Pow(0.95, Time.Current - lastDragTime) * velocity) < velocity_cutoff)
                velocity = 0;

            // Differentiate f(t) = distance * (1 - exp(-t)) w.r.t. "t" to obtain
            // velocity w.r.t. time. Then rearrange to solve for distance given velocity.
            double distance = velocity / (1 - Math.Exp(-DistanceDecayDrag));

            scrollByOffset((float)distance, true, DistanceDecayDrag);
        }

        protected override bool OnScroll(ScrollEvent e)
        {
            if (Content.AliveInternalChildren.Count == 0)
                return false;

            if (parentScrollContainer != null && parentScrollContainer.ScrollDirection != ScrollDirection)
            {
                bool scrollWasMostlyHorizontal = Math.Abs(e.ScrollDelta.X) > Math.Abs(e.ScrollDelta.Y);

                // For horizontal scrolling containers, vertical scroll is also used to perform horizontal traversal.
                // Due to this, we only block horizontal scroll in vertical containers, but not vice-versa.
                if (scrollWasMostlyHorizontal && ScrollDirection == Direction.Vertical)
                    return false;
            }

            bool isPrecise = e.IsPrecise;

            Vector2 scrollDelta = e.ScrollDelta;
            float scrollDeltaFloat = scrollDelta.Y;
            if (ScrollDirection == Direction.Horizontal && scrollDelta.X != 0)
                scrollDeltaFloat = scrollDelta.X;

            scrollByOffset(ScrollDistance * -scrollDeltaFloat, true, isPrecise ? 0.05 : DistanceDecayScroll);
            return true;
        }

        private void onScrollbarMovement(float value) => OnUserScroll(Clamp(FromScrollbarPosition(value)), false);

        /// <summary>
        /// Immediately offsets the current and target scroll position.
        /// </summary>
        /// <param name="offset">The scroll offset.</param>
        public virtual void OffsetScrollPosition(double offset)
        {
            Target += offset;
            Current += offset;
        }

        private void scrollByOffset(double value, bool animated, double distanceDecay = float.PositiveInfinity) =>
            OnUserScroll(Target + value, animated, distanceDecay);

        /// <summary>
        /// Scroll to the start of available content.
        /// </summary>
        /// <param name="animated">Whether to animate the movement.</param>
        /// <param name="allowDuringDrag">Whether we should interrupt a user's active drag.</param>
        public void ScrollToStart(bool animated = true, bool allowDuringDrag = false)
        {
            if (!IsDragging || allowDuringDrag)
                scrollTo(0, animated, DistanceDecayJump);
        }

        /// <summary>
        /// Scroll to the end of available content.
        /// </summary>
        /// <param name="animated">Whether to animate the movement.</param>
        /// <param name="allowDuringDrag">Whether we should interrupt a user's active drag.</param>
        public void ScrollToEnd(bool animated = true, bool allowDuringDrag = false)
        {
            if (!IsDragging || allowDuringDrag)
                scrollTo(ScrollableExtent, animated, DistanceDecayJump);
        }

        /// <summary>
        /// Scrolls to a new position relative to the current scroll offset.
        /// </summary>
        /// <param name="offset">The amount by which we should scroll.</param>
        /// <param name="animated">Whether to animate the movement.</param>
        public void ScrollBy(double offset, bool animated = true) => scrollTo(Target + offset, animated);

        /// <summary>
        /// Handle a scroll to an absolute position from a user input.
        /// </summary>
        /// <param name="value">The position to scroll to.</param>
        /// <param name="animated">Whether to animate the movement.</param>
        /// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
        protected virtual void OnUserScroll(double value, bool animated = true, double? distanceDecay = null) =>
            ScrollTo(value, animated, distanceDecay);

        /// <summary>
        /// Scrolls to an absolute position.
        /// </summary>
        /// <param name="value">The position to scroll to.</param>
        /// <param name="animated">Whether to animate the movement.</param>
        /// <param name="distanceDecay">Controls the rate with which the target position is approached after jumping to a specific location. Default is <see cref="DistanceDecayJump"/>.</param>
        public void ScrollTo(double value, bool animated = true, double? distanceDecay = null) => scrollTo(value, animated, distanceDecay ?? DistanceDecayJump);

        private void scrollTo(double value, bool animated, double distanceDecay = double.PositiveInfinity)
        {
            Target = Clamp(value, ClampExtension);

            if (animated)
                this.distanceDecay = distanceDecay;
            else
                Current = Target;
        }

        /// <summary>
        /// Scrolls a <see cref="Drawable"/> to the top.
        /// </summary>
        /// <param name="d">The <see cref="Drawable"/> to scroll to.</param>
        /// <param name="animated">Whether to animate the movement.</param>
        public void ScrollTo(Drawable d, bool animated = true) => ScrollTo(GetChildPosInContent(d), animated);

        /// <summary>
        /// Scrolls a <see cref="Drawable"/> into view.
        /// </summary>
        /// <param name="d">The <see cref="Drawable"/> to scroll into view.</param>
        /// <param name="animated">Whether to animate the movement.</param>
        public void ScrollIntoView(Drawable d, bool animated = true)
        {
            double childPos0 = GetChildPosInContent(d);
            double childPos1 = GetChildPosInContent(d, d.DrawSize);

            double minPos = Math.Min(childPos0, childPos1);
            double maxPos = Math.Max(childPos0, childPos1);

            if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent))
                ScrollTo(minPos, animated);
            else if (maxPos > Current + DisplayableContent)
                ScrollTo(maxPos - DisplayableContent, animated);
        }

        /// <summary>
        /// Determines the position of a child in the content.
        /// </summary>
        /// <param name="d">The child to get the position from.</param>
        /// <param name="offset">Positional offset in the child's space.</param>
        /// <returns>The position of the child.</returns>
        public virtual double GetChildPosInContent(Drawable d, Vector2 offset) => d.ToSpaceOfOtherDrawable(offset, ScrollContent)[ScrollDim];

        /// <summary>
        /// Determines the position of a child in the content.
        /// </summary>
        /// <param name="d">The child to get the position from.</param>
        /// <returns>The position of the child.</returns>
        public double GetChildPosInContent(Drawable d) => GetChildPosInContent(d, Vector2.Zero);

        private void updatePosition()
        {
            double localDistanceDecay = distanceDecay;

            // If we are not currently dragging the content, and we have scrolled out of bounds,
            // then we should handle the clamping force. Note, that if the target is _within_
            // acceptable bounds, then we do not need special handling of the clamping force, as
            // we will naturally scroll back into acceptable bounds.
            if (!IsDragging && Current != Clamp(Current) && Target != Clamp(Target, -0.01f))
            {
                // Firstly, we want to limit how far out the target may go to limit overly bouncy
                // behaviour with extreme scroll velocities.
                Target = Clamp(Target, ClampExtension);

                // Secondly, we would like to quickly approach the target while we are out of bounds.
                // This is simulating a "strong" clamping force towards the target.
                if ((Current < Target && Target < 0) || (Current > Target && Target > ScrollableExtent))
                    localDistanceDecay = distance_decay_clamping * 2;

                // Lastly, we gradually nudge the target towards valid bounds.
                Target = Interpolation.Lerp(Clamp(Target), Target, Math.Exp(-distance_decay_clamping * Time.Elapsed));

                double clampedTarget = Clamp(Target);
                if (Precision.AlmostEquals(clampedTarget, Target))
                    Target = clampedTarget;
            }

            // Exponential interpolation between the target and our current scroll position.
            Current = Interpolation.Lerp(Target, Current, Math.Exp(-localDistanceDecay * Time.Elapsed));

            // This prevents us from entering the de-normalized range of floating point numbers when approaching target closely.
            if (Precision.AlmostEquals(Current, Target))
                Current = Target;
        }

        protected override void UpdateAfterChildren()
        {
            base.UpdateAfterChildren();

            updateSize();
            updatePosition();

            if (!scrollbarCache.IsValid)
            {
                float size = ScrollDirection == Direction.Horizontal ? DrawWidth : DrawHeight;
                if (size > 0)
                    Scrollbar.ResizeTo(Math.Clamp(AvailableContent > 0 ? DisplayableContent / AvailableContent : 0, Math.Min(Scrollbar.MinimumDimSize / size, 1), 1), 200, Easing.OutQuint);
                Scrollbar.FadeTo(ScrollbarVisible && Precision.DefinitelyBigger(AvailableContent, DisplayableContent, 1f) ? 1 : 0, 200);
                updatePadding();

                scrollbarCache.Validate();
            }

            if (ScrollDirection == Direction.Horizontal)
                Scrollbar.X = ToScrollbarPosition(Current);
            else
                Scrollbar.Y = ToScrollbarPosition(Current);

            ApplyCurrentToContent();
        }

        /// <summary>
        /// This is the final internal step of updating the scroll container, which takes
        /// <see cref="Current"/> and applies it to <see cref="ScrollContent"/> in order to
        /// correctly offset children.
        ///
        /// Overriding this method can be used to inhibit this default behaviour, to for instance
        /// redirect the positioning to another container or change the way it is applied.
        /// </summary>
        protected virtual void ApplyCurrentToContent()
        {
            if (ScrollDirection == Direction.Horizontal)
                ScrollContent.X = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.X));
            else
                ScrollContent.Y = (float)(-Current + (ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y));
        }

        /// <summary>
        /// Converts a scroll position to a scrollbar position.
        /// </summary>
        /// <param name="scrollPosition">The absolute scroll position (e.g. <see cref="Current"/>).</param>
        /// <returns>The scrollbar position.</returns>
        protected virtual float ToScrollbarPosition(double scrollPosition)
        {
            if (Precision.AlmostEquals(0, ScrollableExtent))
                return 0;

            return (float)(ScrollbarMovementExtent * (scrollPosition / ScrollableExtent));
        }

        /// <summary>
        /// Converts a scrollbar position to a scroll position.
        /// </summary>
        /// <param name="scrollbarPosition">The scrollbar position.</param>
        /// <returns>The absolute scroll position.</returns>
        protected virtual float FromScrollbarPosition(float scrollbarPosition)
        {
            if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
                return 0;

            return (float)(ScrollableExtent * (scrollbarPosition / ScrollbarMovementExtent));
        }

        /// <summary>
        /// Creates the scrollbar for this <see cref="ScrollContainer{T}"/>.
        /// </summary>
        /// <param name="direction">The scrolling direction.</param>
        protected abstract ScrollbarContainer CreateScrollbar(Direction direction);

        protected internal abstract partial class ScrollbarContainer : Container
        {
            private float dragOffset;

            internal Action<float> Dragged;

            protected readonly Direction ScrollDirection;

            /// <summary>
            /// The minimum size of this <see cref="ScrollbarContainer"/>. Defaults to the size in the non-scrolling direction.
            /// </summary>
            protected internal virtual float MinimumDimSize => Size[ScrollDirection == Direction.Vertical ? 0 : 1];

            protected ScrollbarContainer(Direction direction)
            {
                ScrollDirection = direction;

                RelativeSizeAxes = direction == Direction.Horizontal ? Axes.X : Axes.Y;
            }

            public abstract void ResizeTo(float val, int duration = 0, Easing easing = Easing.None);

            protected override bool OnClick(ClickEvent e) => true;

            protected override bool OnDragStart(DragStartEvent e)
            {
                if (e.Button != MouseButton.Left) return false;

                dragOffset = e.MousePosition[(int)ScrollDirection] - Position[(int)ScrollDirection];
                return true;
            }

            protected override bool OnMouseDown(MouseDownEvent e)
            {
                if (e.Button != MouseButton.Left) return false;

                dragOffset = Position[(int)ScrollDirection];
                Dragged?.Invoke(dragOffset);
                return true;
            }

            protected override void OnDrag(DragEvent e)
            {
                Dragged?.Invoke(e.MousePosition[(int)ScrollDirection] - dragOffset);
            }
        }

        public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
        {
            if (!IsHandlingKeyboardScrolling)
                return false;

            switch (e.Action)
            {
                case PlatformAction.MoveBackwardLine:
                    ScrollToStart();
                    return true;

                case PlatformAction.MoveForwardLine:
                    ScrollToEnd();
                    return true;

                default:
                    return false;
            }
        }

        public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
        {
        }
    }
}
